package com.talkyun.openx.ocean.wrapper;

import com.talkyun.openx.RestfulHeaderManager;
import com.talkyun.openx.error.ExceptionHandler;
import com.talkyun.openx.ocean.codec.RegistryableJsonCodec;
import com.talkyun.openx.server.AbstractHandler;
import com.talkyun.openx.server.core.*;
import com.talkyun.utils.json.JSON;
import com.talkyun.utils.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.Map;

import static com.talkyun.openx.server.core.ServiceContext.REQ_HOST;
import static com.talkyun.openx.server.core.ServiceContext.REQ_IP;
import static com.talkyun.openx.server.core.ServiceResponse.Status.ERROR;
import static com.talkyun.openx.server.core.ServiceResponse.Status.NORMAL;
import static javax.servlet.http.HttpServletResponse.*;

/**
 * @author kevin
 */
public class RegistryableServletHandler extends AbstractHandler {
    private static final String CHARSET = "utf-8";
    private static final Logger logger = LoggerFactory.getLogger(RegistryableServletHandler.class);

    private static final ExceptionHandler handler = new ExceptionHandler();

    private String context;
    private RegistryableServiceFactory factory;
    private RegistryableServiceInvoker invoker;
    private RegistryableJsonCodec codec;

    public RegistryableServletHandler(ServletContext sc, RegistryableServiceFactory serviceFactory) {
        context = sc.getContextPath();
        factory = serviceFactory;
        codec = new RegistryableJsonCodec(context, factory);
        invoker = new RegistryableServiceInvoker(factory);
        logger.info("Create ServletHandler: {} ", isBlank(context) ? "/" : context);
        // factory.add("/openx/echo", new EchoImpl());
    }

    public void handle(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 0. cors first
        String method = req.getMethod().toLowerCase();
        this.doCors(req, resp);
        if ("options".equals(method)) {
            return;
        }

        // 1. servlet handle
        String uri = this.getLastURI(req.getRequestURI(), context);
        String data = this.doRead(req);
        String session = req.getSession().getId();
        //   - ready service request
        ServiceRequest sr = codec.decode(uri, data, session);

        //   1.1 - not found service impl, response 404
        if (sr == null) {
            logger.warn("Not found rest request! {} ", uri);
            this.doWrite(resp, SC_NOT_FOUND, "{\"message\":\"" + uri + " not found!\"}");
            return;
        }
        //   - ready service context
        String host = this.getRemoteHost(req);
        String reqIp = this.getRemoteAddress(req);
        ServiceContext ctx = ServiceContext.getContext(session);
        ctx.put(REQ_IP, reqIp);
        ctx.put(REQ_HOST, host);
        //   - set request to ctx
        ctx.setServiceRequest(sr);

        // ADD header through
        this.doHeaderThrough(req);

        // 2. service invoker
        ServiceResponse sp = this.invoke(ctx, sr);
        ctx.clean();

        // 3. servlet response
        this.doResponse(resp, sp);
    }

    private ServiceResponse invoke(ServiceContext ctx, ServiceRequest sr) {
        try {
            return invoker.invoke(ctx, sr);
        } catch (Throwable t) {
            // handle throwable, wrap to response
            if (t instanceof ServiceException) {
                t = t.getCause(); // get raw throwable
            } else if (t instanceof InterceptorException && t.getCause() != null) {
                t = t.getCause();
            }
            return new ServiceResponse(sr.getSession(), ERROR, handler.encode(t));
        }
    }

    private void doCors(HttpServletRequest req, HttpServletResponse resp) {
        String origin = req.getHeader("Origin");
        if (null == origin || "null".equalsIgnoreCase(origin)) {
            resp.addHeader("Access-Control-Allow-Origin", "*");
        } else {
            resp.setHeader("Access-Control-Allow-Origin", origin);
        }
        resp.addHeader("Access-Control-Manager", "GateWay-1");
        resp.addHeader("Access-Control-Allow-Methods", "GET,POST");
        resp.addHeader("Access-Control-Allow-Headers", "Set-Cookie,X-Requested-With,Content-Type,X-Next-Token");
        resp.addHeader("Access-Control-Allow-Credentials", "true");
    }

    private void doResponse(HttpServletResponse resp, ServiceResponse sp) throws IOException {
        String json = sp.getResult();
        if (sp.getStatus() == NORMAL) {
            this.doWrite(resp, SC_OK, json);
        } else {
            this.doWrite(resp, SC_INTERNAL_SERVER_ERROR, json);
        }
    }

    private void doWrite(HttpServletResponse resp, int code, String json) throws IOException {
        resp.setStatus(code);
        resp.setCharacterEncoding(CHARSET);
        resp.setContentType("application/json");
        resp.setHeader("Cache-Control", "no-cache");
        resp.getWriter().print(json);
    }

    private String doRead(HttpServletRequest req) throws IOException {
        if ("GET".equalsIgnoreCase(req.getMethod())) {
            // read from get url
            String data = req.getQueryString();
            if (!super.isBlank(data)) {
                return this.doReadQueryString(data);
            }
            return data;
        } else {
            // read from post stream
            return super.read(req.getInputStream(), CHARSET);
        }
    }

    private String doReadQueryString(String qs) throws UnsupportedEncodingException {
        JSONObject json = JSON.parseObject("{}");

        String[] parts = qs.split("&");
        for (String part : parts) {
            if (super.isBlank(part)) {
                continue;
            }
            String[] kv = part.split("=");
            if (kv.length != 2) {
                continue;
            }
            if (!json.exists(kv[0].trim())) {
                json.put(kv[0].trim(), URLDecoder.decode(kv[1], CHARSET).trim());
            }
        }

        return json.toString();
    }

    private void doHeaderThrough(HttpServletRequest req) throws IOException {
        Enumeration<String> hns = req.getHeaderNames();
        if (hns == null || !hns.hasMoreElements()) {
            return;
        }

        Map<String, String> throughHeader = RestfulHeaderManager.get().getOnceHead();
        while (hns.hasMoreElements()) {
            String name = hns.nextElement();
            if (isBlank(name)) {
                continue;
            }

            String head = name.trim().toLowerCase();
            if (!head.startsWith("x-")) {
                continue;
            }

            String value = req.getHeader(name);
            if (isBlank(value)) {
                continue;
            }

            throughHeader.put(name.trim(), value.trim());
        }
    }

    private String getRemoteHost(HttpServletRequest req) {
        String host = req.getHeader("Host");
        if (!super.isBlank(host)) {
            return host;
        }
        return "-";
    }

    private String getRemoteAddress(HttpServletRequest req) {
        String ip = req.getHeader("X-Forwarded-For");
        // proxy mode (slb|nginx)
        if (!isBlank(ip)) {
            int pos = ip.indexOf(",");
            if (pos != -1) {
                return ip.substring(0, pos).trim();
            } else {
                return ip.trim();
            }
        }
        // not proxy mode
        return req.getRemoteAddr();
    }
}